A few things you can try...
Share your vertices
You mention you have 16x16 grids of 1024 vertices. But 16x16 is only 256 cells. So it sounds like you're rendering 4 separate vertices for each square of your grid. But 4 adjacent grid squares all share one vertex at their shared corner, so we can use this fact to use our vertex bandwidth more efficiently.
We could have 32 vertices along the x, and 32 vertices along the y, for a total of 1024 vertices and 1922 triangles (31x31x2)
More aggressive LOD reduction
In a previous answer I recommended halving your grid resolution every time you double your distance, as a way to get approximately even polygon-to-pixel coverage in each detail band. But this is just a guideline.
You could instead choose to halve the resolution each time the distance multiplies by 1.5 - making noticeably larger polygons in the far distance, but these are generally the least important for gameplay, and often get occluded by nearer details, so they're the safest place to drop stuff down.
Draw your closest / highest resolution chunks first. This populates the depth buffer with the closest terrain, so that terrain rendered later will often find itself behind an already-rendered surface, letting the graphics card skip it through an early-Z rejection, so it never has to invoke the fragment shader at all for this bit of terrain.
This doesn't mean you have to draw every polygon in front-to-back order (micromanaging the draw order like this can make it hard to get good batching performance), but even just breaking your terrain into coarse bands and drawing the near band of chunks first can give you substantial reductions in overdraw.
I assume you're already culling chunks outside the camera's frustum (out of view beside/behind the camera, or out beyond the far plane), along with any chunks that are out of view because they're below the horizon from the current viewing angle.
You can use some tricks to bring the rendering horizon closer in order to cull even more.
On the earth, an observer at human height can see about 4.7 km on a clear day, but lots of things can mask that far detail. Atmospheric effects can mask or diminish distant objects, and local geography can occlude more distant landmarks.
So, you could be justified in pulling in your rendering horizon to more like 2-3 km, and culling chunks outside that range. To avoid popping, you can fade the most distant chunks into your skybox as they approach the cut-off, dither them out, or scale them down to 0 height gradually, to help tall-but-distant mountain peaks roll out of view behind closer hills.
On Starlink Battle for Atlas, our planets are actually tiny by the standards of real star systems, and we use vertex shader tricks to bend the horizon to where we want it for our gameplay experience and rendering budget. ;)
If a lot of your frame time is spent iterating over your chunks and deciding which ones need to be up-rezzed or combined, or actually building the new chunk versions (profile and find out), this may be work you can offload.
Set aside a thread to update your chunk information based on the latest camera position, and write it to a second buffer alongside the one the main thread is currently reading/drawing. At the start of each frame, the main thread just picks the latest version of the buffer and works from that, freeing the old buffer for a fresh update pass on the other thread.
You can also enforce a budget of how many chunks you'll regenerate in a single pass, and place them in a queue to be served on the next pass, so you can keep your peak recalculation time bounded, minimizing overall terrain update latency.
It might mean a chunk uprezzes/downrezzes a frame or two later than it should, but unless you're rocketing through the scene this shouldn't be too noticeable (and if you are going that fast, a little motion blur or VFX can help hide your sins).
Use bigger chunks
You mention each grid is 16 x 16. This is extremely small by the usual standards of game terrains. If you're submitting each chunk to the GPU as a separate draw call, then you're spending a lot of your time in the overhead of all these calls and the state switches between them, and giving the GPU relatively little to chew on in each call.
Graphics cards tend to be optimized for the opposite: a smaller number of draw calls, with big batches of data to grind through in each call.
If you're drawing an indexed triangle list with unsigned shorts for your indices, you could fit up to 256 x 256 vertices into a single chunk / a single draw call. Not that this is necessarily the best size (bigger chunks also means less granularity in the border between high & low detail areas, so you might keep more polygons at a redundantly high level of detail than what you strictly need), but it gives you a range to experiment within.
Combine your draw call batches
Look for ways that you don't need to submit each chunk as its own separate draw call.
This could include setting aside vertex buffers as big as you can make them, then packing multiple chunks into them (eg. if your chunks are 1024 vertices each, you could pack 64 chunks into a single buffer of 65k vertices, and draw them all in a single pass) When a chunk is regenerated, mark its space in the buffer unused, and write its successor(s) in unused buffer slots.
Or you could use GPU tessellation or compute capabilities to send only sparse information to the GPU - like the corners of the chunks or of your rendered area - and let GPU generate all the intermediate triangles within the chunks for you. Then you might be able to submit a single draw call that just says where all the chunks are, and draw all the chunks in one go.
Instead of storing and transferring a full 3-component position, normal, and UV vector for every vertex in the terrain, you might be able to store just a buffer/texture of height values, with the rest determined implicitly as the GPU generates the intermediate vertices, cutting down on memory & bandwidth use and letting you pack more into a single pass (like drawing an 8k x 8k texture's worth of heightmap information all in one go, without also maintaining & sending 67 million vertices' worth of index buffers)
I can't speak to the specific techniques that would be best for your context, but other users may be able to elaborate on strategies to use here.