Make a big 3D model of your map. Then, cut it into pieces. The size of the pieces doesn't matter all that much, but it'll be an inverse relationship between memory and speed. The bigger your "chunks" are, the more memory it will take, but the less often you'll have to load new ones. It's the opposite for small chunks.
In the code itself, only load the chunks closest to the player. As the player moves in a direction, load the chunks in that direction so that the map always appears roughly the same size.
One way that pops to mind immediately is to wait for a player to cross the border of a chunk, then load whatever chunk is next in that direction, and unload whatever loaded chunk is farthest in the opposite. Some pseudocode(ish) to illustrate what I mean:
void onPlayerMove(Player player):
if (player.isOnCurrentChunk) break
if (player.xPos > player.currentChunk.xMax):
else if (player.xPos < player.currentChunk.xMin):
else if (player.yPos > player.currentChunk.yMax):
else if (player.yPos < player.currentChunk.yMin):
loadChunk(direction) would load a chunk from a file or something and place it in the appropriate spot.
unloadChunk(direction) would find the chunk that's farthest in that direction and unload it from memory, saving it to disk if it might be changed so that those changes persist. Or not, if you don't want changes to persist.
Both could also load/unload the entire row of chunks in that direction, the chunk and the ones adjacent to it, whatever; the implementation is up to you. You could also use different boundaries for the chunks, but that might be more annoying to implement, since you'd have to separate the loading/unloading to prevent it from spiraling out of control.
moveCurrentChunk(direction) would change the chunk that the player is supposed to be changing on to the adjacent one in X direction.
This whole thing is looped so that if someone goes off the corner, it records them as going off in both directions, as opposed to one arbitrary one. In addition, this will automatically move to wherever they are, one chunk at a time, if you have some mechanism by which they can move more than one chunk at a time and you don't want to implement some special function for them.
You could probably also do something with their speed; loading the chunk early if they're going fast (e.g. on a mount or the like), or loading more than one at a time. Again, that's all up to you.
Hope this helped.