On separating threads and the slow client
First of all. Yes, there is a way to do it all in a single thread, and it will work. Will come back to the problem of the slow client.
Simulation Thread and Network Thread
If the simulation is running on a separate thread it should not be dealing with connections. Instead it need a way to get all the network input since last iteration.
One way to do that is to have the network thread write to an network update object that holds a datastructure where all the network events are stored (commands from clients, disconnections, and so on). Then, when the simulation thread wants to get them, it can swap the object for a new one (which can be done atomically).
That also means that the network thread cannot be allowed to cache the reference to the network update object. Thus, all access to the network update object will have to be volatile.
Another way to do that is to have "channel" objects. Either a ConcurrentBag<T> or a ConcurrentQueue<T> will work (they are not so different). I would avoid using the BlockingCollection<T> for this purpose (because it is blocking). Then the Network thread will push/add into the channel the network events, and the simulation thread can pick them up when it loops.
Note: just because you used one way to get input from the network, does not mean you have to use the same way to send updates. In fact, sending updates has extra considerations. However, I would recommend to start by making the communication between simulation and network symmetric.
On the slow client problem
Please note that the slow client problem can still have an impact even if you separate simulation and network, because if the network is waiting on a client, it will not take input from the other clients.
Also note that you will inevitably open a socket to receive. Unless you are using a library designed for this, I recommend to stay in control of the socket.
With that in mind, you do not have to use a blocking API to receive. Regardless of how many threads the server is using. Ideally you use something like ReceiveAsync that gives you a Task.
Once you have a Task that will complete when you receives data from a client, you have a few options...
First option:
You can Wait (not await) with a timeout (and even a CancellationToken, useful to teardown the server). That way you know that regardless how slow the client is, the network thread will not be stuck for long.
You can then, each loop, check if the task completed. If it did, take the result, and recreate the task. That is how you do it in a single thread.
Note: When you are not receiving, the socket is buffering.
Second Option:
You can add a continuation, or better yet, wrap it all in an async method. Yes, I said to not await the task, that is so you can pass parameters to Wait, but that does not mean you cannot wrap it in an async method. - then that continuation will take the input from the network, pass it to whatever object you need it into (for example a channel object).
Each loop, you will still check if you need to recreate the task.
Third option:
Following the same idea, you can recreate the task inside of the continuation. So that you do not have to worry about it each loop.
In fact, if you started with a single threaded solution and moved down this path. From this point, it just a matter of using a well placed Task.Run (if you did not do it already) and you have a separated thread for the network (if you are using Task.Run mark it LongRunning, so it is a dedicated thread and not one from the ThreadPool).