While this has a loop similar to other answers, a minor difference is the selection of the opening and closing tags - instead of calculating modulo 2 to determine if we use an opening or closing tag, I define the pair of tags in an array of size 2: 
String[] replaceText = new String[] { "<B>", "</B>" };
Select the element based on a variable iReplacerIndex 
replaceText[iReplacerIndex]  // Gives either the opening or closing tag 
And toggle between the required values by subtracting from 1.
iReplacerIndex = 1 - iReplacerIndex; // If last used was an opening tag, 
                                     // then next required is a closing tag 
The above also makes it easy to check for mismatched tags - if iReplacerIndex is not 0 after the loop, then there is a mismatched tag. 
The entire code is as follows (its lengthier than it needs to be for clarity): 
String sourceText = "My '''random''' text with '''bold''' words.";
int sourceLength = sourceText.Length;
String searchText = "'''";
int searchLength = searchText.Length;
String[] replaceText = new String[] { "<B>", "</B>" };
int iReplacerIndex = 0
  , iStartIndex = 0 
  , iStopIndex = sourceText.Length - 1;
System.Text.StringBuilder sbCache = new System.Text.StringBuilder(sourceText.Length * 2);
do
{
    iStopIndex = sourceText.IndexOf(searchText, iStartIndex);
    if (iStopIndex == -1)
    {
        sbCache.Append(sourceText.Substring(iStartIndex, sourceLength - iStartIndex));
    }
    else
    { 
    sbCache.Append(sourceText.Substring(iStartIndex, iStopIndex - iStartIndex));
    sbCache.Append(replaceText[iReplacerIndex]);
    iReplacerIndex = 1 - iReplacerIndex;
    iStartIndex = iStopIndex + searchLength;
    }
} while (iStopIndex != -1);
Console.WriteLine(sbCache.ToString());