It's worth considering Base 64 instead of Hex as the intermediate representation here. The Hex representation is about 1.5 times larger than the Base 64, so Base 64 is better for avoiding the Apex heap limit.
So, the above example becomes:
Blob b1 = Blob.valueOf('s1');
Blob b2 = Blob.valueOf('s2');
Blob b3 = Blob.valueOf('s3');
String combinedDataAsBase64 = EncodingUtil.base64Encode(b1) + EncodingUtil.base64Encode(b2) + EncodingUtil.base64Encode(b3);
Blob combinedDataAsBlob = EncodingUtil.base64Decode(combinedDataAsBase64);
This only works if the blobs produce no padding in their Base64 form, which may be true for some applications where we can choose the size of the chunks (e.g. handling file uploads in chunks from the Lightning UI) but it is not always the case.