Loops to merge documents and renumber pages
One way to make it easier to follow is to construct your merged documents result separately from your input (above code is not tested).

Construct a dictionary to easily get the document to append to, based on type. It also ensures a fast operation. For each document from the input, append its pages in the dictionary:

// type =&gt; merged document mapping
var mergedDocsDict = new Dictionary&lt;String, Document&gt;();

response.documents.ForEach(doc =&gt; 
    {
       if (!mergedDocDict.ContainsKey(doc.docType))
          mergedDocDict.Add(doc.docType, new Document());

       mergedDocDict[doc.docType].pages.AddRange(doc.pages);
    };


Finally renumbering all the pages:

mergedDocDict.Keys.ForEach(key =&gt; 
   { 
       var doc = mergedDocDict[key];
       int pageCount = doc.pages.Count;
       for (int i = 0; i &lt; pageCount; i ++)
          doc.pages[i].pageNo = i + 1;
   };


You result can be obtain from the dictionary:

mergedDocDict.Values.ToList();


This solution should be easier to understand, but uses extra memory for the result.

This can be overcome, by reusing the same documents:

response.documents.ForEach(doc =&gt; 
    {
       if (!mergedDocDict.ContainsKey(doc.docType))
          mergedDocDict.Add(doc.docType, doc);
       else
          mergedDocDict[doc.docType].pages.AddRange(doc.pages);
    };


Also, page renumbering can be done in the first loop, obtaining a single foreach:

response.documents.ForEach(doc =&gt; 
    {
       if (!mergedDocDict.ContainsKey(doc.docType))
          mergedDocDict.Add(doc.docType, doc);
       else
       {
          int existingCount = mergedDocDict[doc.docType].pages.Count;
          mergedDocDict[doc.docType].pages.AddRange(doc.pages);
          int newCount = doc.pages.Count + existingCount;
          for (int i = existingCount; i &lt; newCount; i ++)
            mergedDocDict[doc.docType].pages[i].pageNo = i + 1; 
       }
    };


Complexity should be O(number_of_docs * number_of_pages) as all dictionary operations are done in O(1).

If your documents were copied from some other data structure to your response, I think it is better to construct the dictionary from that data structure and obtain respose.documents from the merged data. Otherwise, you have to use a for loop to remove documents already merged (somewhat more convoluted than foreach version).